package com.norriekoelle.virtualsensors.server;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.Enumeration;

import android.text.format.DateFormat;
import android.util.Log;

import com.norriekoelle.virtualsensors.shared.Receiver;

/**
 * Manager for the wifi connection to the server.
 * 
 * Note: Internet permissions in manifest
 * 
 * @author Lauren Norrie (norrielm)
 */
public class WifiClient {
	
	private static final String LOG = "WifiClient";
	private Socket echoSocket = null;
	private DatagramSocket udpSocket;
    private DataInputStream is;
	private DataOutputStream os;
	public boolean wifiAvailable = false;
    public boolean serverConnected=false;
    private boolean connected = false;
	public boolean exiting = false;
	// TODO(norrielm): Allow to be set somewhere by the device.
	private String serverAddr = "10.10.10.26";
	private int port = 55555;
	private final String ID = "Device";
	protected boolean reconnecting = false;
	private Receiver<String> messageReceiver;
	private Receiver<String> commandHandler;
	private Runnable disconnectedCallback;
	private Runnable exitingCallback;
	private Runnable noNetworkCallback;
	
	public WifiClient(Runnable disconnectedCallback, Runnable exitingCallback, 
    		Runnable noNetworkCallback, Receiver<String> messageReceiver, 
    		Receiver<String> commandHandler) {
		this.messageReceiver = messageReceiver;
		this.commandHandler = commandHandler;
		this.disconnectedCallback = disconnectedCallback;
		this.exitingCallback = exitingCallback;
		this.noNetworkCallback = noNetworkCallback;
    }
    
    /** 
     * Get the client IP address or return null if not connected.
     */
    private String getLocalIpAddress() throws SocketException {
        for (Enumeration<NetworkInterface> networkInterfaces = 
        		NetworkInterface.getNetworkInterfaces(); networkInterfaces.hasMoreElements();) {
            NetworkInterface networkInterface = networkInterfaces.nextElement();
            for (Enumeration<InetAddress> ipAddr = networkInterface.getInetAddresses(); 
            		ipAddr.hasMoreElements();) {
                InetAddress inetAddress = ipAddr.nextElement();
                if (!inetAddress.isLoopbackAddress()) { 
                	return inetAddress.getHostAddress().toString(); 
            	}
            }
        }
        return null;
    }
    
    public void start() {
    	try {
    		getLocalIpAddress();    		
    		connectToServer();    	
    	} catch (SocketException e) {
            Log.e(LOG, e.toString());
        } catch(Exception e){
            Log.e(LOG, e.toString());        	
    	} finally {
        	wifiAvailable = false;
        	noNetworkCallback.run();
    	}	
    }

    public void connectToServer() throws UnknownHostException, IOException {
    	Log.d(LOG, "Starting connection threads...");
		connected = false;
		reconnecting = false;
		exiting = false;
		serverConnected = false;

        new Thread (new Runnable(){
			@Override
			public void run() {
				// TODO(norrielm): busy wait while not connected. This is bad. 
				while (!connected) {
					try { 
						Thread.sleep(500);
					} catch (InterruptedException e) {}
				}			
				Log.i(LOG, "Local connection to socket established. " +
						"Waiting for server connection ack.");
				while (!exiting || !reconnecting) {
					receiveUDPMessage();
					try{
						Thread.sleep(20);
					} catch(InterruptedException e) {}
				}
			}        	
        }).start();
        
        // TODO(norrielm): Can probably pull these runnables out.
        new Thread(new Runnable(){
			@Override
			public void run() {
				// TODO(norrielm): Now that we're using UDP we should assume that there is no 
				// connection and can possible remove this part.
				while (!connected && !exiting) {
			    	try {
						udpSocket = new DatagramSocket();
						connected = true;
						Log.d(LOG, "Socket created");
					} catch (SocketException e) { }
				}
				messageReceiver.accept("connecting...");
				sendUDPMessage("close", "closing");
				// Prompt server to send a message.
				while (!exiting && !serverConnected) {
					sendUDPMessage("", "connect\n");
					try{
						Thread.sleep(4000);
					} catch(InterruptedException e) {}
				}
				Log.i(LOG, "Connection established with the Kinect server.");
			}
        }).start();
    }

    public void connected() {
    	serverConnected = true;
    	messageReceiver.accept("connected.");
    }
    
    public boolean isConnected() {
    	return serverConnected;
    }
    
    public void reconnect(){
    	reconnecting = true;
		udpSocket.close();
		
    	disconnectedCallback.run();
    	messageReceiver.accept("reconnecting");
    	
    	try {
			connectToServer();
		} catch (UnknownHostException e) {
			messageReceiver.accept("UnknownHost: Reconnect failed.");
		} catch (IOException e) {
			messageReceiver.accept("IOException: Reconnect failed.");
		}
    }
    
	public void closeServer() {
		exiting = true;
		Log.d(LOG, "closing server");
		sendUDPMessage("close", "closing");
		sendUDPMessage("close", "closing");
		sendUDPMessage("close", "closing");
		
		try {
			if (os!=null)
				os.close();			
			if (is!=null)
				is.close();
			if (echoSocket!=null)
				echoSocket.close();
			if (udpSocket!=null)
				udpSocket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Send a message into the UDP socket 
	 */
	public void sendUDPMessage(String command, String message) {
		if (udpSocket != null) {
			//tell everyone we're sending a message
			Log.d(LOG, "sending "+message);
			message= String.format("%s %s %s\n", command, ID, message).trim();
			Log.d(LOG, "sending : " + message );
			
			byte[] buf = new byte[1024];
			buf = message.getBytes();
			DatagramPacket packet;
			try {
				packet = new DatagramPacket(buf, buf.length, InetAddress.getByName(serverAddr), 
					port);
				udpSocket.send(packet);
			} catch (UnknownHostException e1) {
				Log.d("AndroidClient", "unknown host");
			}catch (IOException e) {
				Log.d("AndroidClient", "io ex");
			}
		}
	}
	
	/**
	 * Blocking receive from the UDP socket
	 */
	public void receiveUDPMessage(){
		if (udpSocket!=null && !udpSocket.isClosed()) {
			Log.d(LOG, "receive..." );
			byte[] buf = new byte[30];
			DatagramPacket packet = new DatagramPacket(buf, buf.length);	        
	        try {
				udpSocket.receive(packet);
			} catch (IOException e) {
				Log.d("UDP", "S: err...");
				e.printStackTrace();
			}

	        String serverMessage = new String(packet.getData()).toLowerCase();
	        String time = "(" + DateFormat.format("hh:mm:ss", new Date()) + ")"; 
			Log.d(LOG, time + " received " + serverMessage );
			handleMessage(serverMessage);
		}
	}
	
	public boolean handleMessage(String serverMessage){
		Log.d(LOG, serverMessage);
		CommandInterface.commands c = CommandInterface.handleKinectMessage(serverMessage);
		
		switch(c) {
		case EXIT:
				exitingCallback.run();
				reconnect();				
				return true;
		case CONNECT:
				Log.d(LOG, c.toString());
				sendUDPMessage("", "ack");
	    		connected();
	    		return true;
	    default:
	    	commandHandler.accept(serverMessage);
	    	return true;
		}
	}
    
    public void setMessageReceiver(Receiver<String> receiver) {
    	this.messageReceiver = receiver;
    }
    
    public void setCommandHandler(Receiver<String> handler) {
    	this.commandHandler = handler;
    }

    public void setDisconnectedCallback(Runnable callback) {
    	this.disconnectedCallback = callback;
    }

    public void setExitingCallback(Runnable callback) {
    	this.exitingCallback = callback;
    }

    public void setNoNetworkCallback(Runnable callback) {
    	this.noNetworkCallback = callback;
    }
}
